<?php
/**
 *  TinyMVC - TinyMVCDAO.class.php file
 *
 * @author Adrian Wieczorek <adi(at)tinymvc(dot)org>
 * @package TinyMVC
 * @subpackage Database
 * @version $Id: MysqlDAO.class.php 95 2010-12-29 13:27:38Z adi3ek $
 *
 */

/**
 * Simple DAO class implementation
 *
 * @package TinyMVC
 * @subpackage Database
 */
abstract class TinyMVCDAO {
	/**
	 * db row id / object unique id
	 * @var type
	 */
	private $id = 0;
	/**
	 * object class name
	 * @var string
	 */
	protected $className = null;
	/**
	 * db driver object
	 * @var MysqlDriver
	 */
	private $db = null;
	/**
	 * cache engine object
	 * @var Cache
	 */
	private $cache = null;
	/**
	 *
	 */
	private $cacheTTL = 60;
	/**
	 * db row field => value array
	 * @var array
	 */
	protected $fields = array();
	/**
	 * db tables representing the object
	 * @var type
	 */
	private $tables = array();

	protected function __construct($DAOId = 0, $forUpdate = false) {
		if(count($this->tables) == 0) {
			throw new TinyMVCException("TinyMVCDAO - DB tables not defined for DAO, use addTable() method first");
		}

		if(!empty($DAOId)) {
			$this->read($DAOId, $forUpdate);
		}
	}

	public static function newFromId($className, $DAOId = 0, $forUpdate = false, TinyMVCCache $cache = null) {
		if(empty($DAOId) || $forUpdate || is_null($cache)) {
			$object = self::newFromDb($className, $DAOId, $forUpdate, $cache);
		}
		else {
			$object = self::newFromCache($className, $DAOId, $cache);
			if(empty($object)) {
				$object = self::newFromDb($className, $DAOId, $forUpdate, $cache);
			}
		}

		return $object;
	}

	protected static function newFromDb($className, $DAOId = 0, $forUpdate = false, TinyMVCCache $cache = null) {
		$object = new $className($DAOId, $forUpdate); // we can't use Factory here as some classes might have default constructor private
		if($cache instanceof TinyMVCCache) {
			$object->setCache($cache);
			$object->toCache();
		}
		return $object;
	}

	protected static function newFromCache($className, $DAOId, TinyMVCCache $cache) {
		$cachedObject = null;
		if(!empty($DAOId)) {
			$cachedObject = $cache->get(self::getCacheKey($DAOId, $className));
			if($cachedObject instanceof TinyMVCDAO) {
				$cachedObject->setCache($cache);
			}
		}

		return $cachedObject;
	}

	/**
	 * send object to cache
	 */
	public function toCache() {
		if(($this->cache instanceof TinyMVCCache) && !empty($this->id)) {
			$cache = $this->cache;

			$this->db = null;
			$this->cache = null;

			$result = $cache->set(self::getCacheKey($this->getId(), $this->className), $this, $this->getCacheTTL());

			$this->cache = $cache;
			return $result;
		}
		return false;
	}

	/**
	 * remove object from cache
	 */
	public function deleteFromCache() {
		if(($this->cache instanceof Cache) && !empty($this->id)) {
			return $this->cache->delete(self::getCacheKey($this->getId(), $this->className));
		}
		return false;
	}


	/**
	 * get cache key for current object
	 * @param int $id object id
	 * @param string $className object class name
	 * @return string
	 */
	public static function getCacheKey($id, $className) {
		return $className . ':' . $id;
	}

	public function getCacheTTL() {
		return $this->cacheTTL;
	}

	public function setCacheTTL($value) {
		$this->cacheTTL = $value;
	}

	/**
	 * reload object data from db using FOR UPDATE statement
	 */
	public function forUpdate() {
		if($this->getId()) {
			$this->read($this->getId(), true);
		}
		return $this;
	}

	/**
	 * object unique id
	 * @return int
	 */
	public function getId() {
		return $this->id;
	}

	/**
	 * get db driver
	 * @return MysqlDriver
	 */
	public function getDb() {
		if($this->db == null) {
			$this->db = TinyMVCFactory::build('Database');
		}
		return $this->db;
	}

	/**
	 * set db driver
	 * @param MysqlDriver $db
	 */
	public function setDb(MysqlDriver $db) {
		$this->db = $db;
	}

	/**
	 * get cache engine
	 * @return TinyMVCCache
	 */
	public function getCache() {
		return $this->cache;
	}

	/**
	 * set cache engine
	 * @param TinyMVCCache $cache
	 */
	public function setCache(TinyMVCCache $cache) {
		$this->cache = $cache;
	}

	/**
	 * get all row fields
	 * @return array field => value array
	 */
	public function getFields() {
		return $this->fields;
	}

	/**
	 * get value for given field
	 * @param string $field field name
	 * @return mixed field value
	 */
	public function get($field) {
		return $this->fields[$field];
	}

	/**
	 * set field value
	 * @param string $field field name
	 * @param string $value field value
	 *
	 */
	public function set($field, $value) {
		$this->fields[$field] = $value;
	}

	public function getClassName() {
		return $this->className;
	}

	public function setClassName($className) {
		$this->className = $className;
	}

	public function addTable(MysqlTable $table) {
		$this->tables[$table->getName()] = $table;
	}


	public function setExistingTableAsParent($tableName, 	MysqlTable $childTable, $parentColumn, $childColumn) {
		if(isset($this->tables[$tableName])) {
			$parentTable = $this->tables[$tableName];
			$this->tables[$childTable->getName()] = $childTable->setParentTable($parentTable, $parentColumn, $childColumn);
			unset($this->tables[$tableName]);
		}
		else {
			throw new TinyMVCException("TinyMVCDAO - table $tableName hasn't been defined");
		}
	}

	public function read($DAOId, $forUpdate = false) {
		$this->preRead();
		foreach($this->tables as $table) {
			$success = $table->runSelectStatement($this->getDb(), $DAOId, $this->fields, $forUpdate);
			if($success) {
				$this->id = $DAOId;
			}
		}
		if(empty($this->id)) {
			throw new 	TinyMVCException("TinyMVCDAO - read() failed, no data fom given object ID has been found");
		}
		$this->postRead();

		if(!$forUpdate) {
			$this->toCache();
		}
	}

	public function update() {
		$this->preUpdateOrCreate();
		if(empty($this->id)) {
			$this->create();
		}
		else {
			$this->preUpdate();
			foreach($this->tables as $table) {
				$table->runUpdateStatement($this->getDb(), $this->fields);
			}
			$this->postUpdate();
		}
		$this->postUpdateOrCreate();
		$this->toCache();
	}

	private function create() {
		$this->preCreate();
		$DAOId = 0;
		foreach($this->tables as $table) {
			$DAOId = $table->runInsertStatement($this->getDb(), $this->fields);
			if(!empty($DAOId)) {
				$this->id = $DAOId;
			}
		}
		if(empty($DAOId)) {
			throw new TinyMVCException("TinyMVCDAO - create() failed, no object ID has been returned");
		}

		// perform read() action to fetch real data from db (e.g. field defaults)
		$this->read($DAOId);

		$this->postCreate();
	}

	public function delete() {
		if(!empty($this->id)) {
			$this->preDelete();
			foreach($this->tables as $table) {
				$table->runDeleteStatement($this->getDb());
			}
			$this->id = 0;
			$this->postDelete();
			$this->deleteFromCache();
		}
	}

	protected function getAsObject(&$property, $fieldName, $className) {
		if($property instanceof $className) {
			return $property;
		}
		else {
			$property = new $className(!empty($this->fields[$fieldName]) ? $this->fields[$fieldName] : 0);
			return $property;
		}
	}

	protected function getAsObjectFromFactory(&$property, $className, $factoryBuildMethod, Array $constructorParams, $constructorMethod = '__construct') {
		if($property instanceof $className) {
			return $property;
		}
		else {
			$buildParams = array( $className, $constructorParams, $constructorMethod );
			$property = call_user_func_array($factoryBuildMethod, $buildParams);
			return $property;
		}
	}

	protected function setFromObject(&$property, $object, $fieldName) {
		$this->fields[$fieldName] = $object->getId();
		$property = $object;
	}

	public function getCached() {
		if($this->cache instanceof TinyMVCCache) {
			return $this->cache->get(self::getCacheKey($this->getId(), $this->className));
		}
	}

	/**
	 * pre create/read/update/delete methods
	 */
	protected function preCreate() { }
	protected function preRead() { }
	protected function preUpdate() { }
	protected function preDelete() { }
	protected function preUpdateOrCreate() { }

	/**
	 * post create/read/update/delete methods
	 */
	protected function postCreate() { }
	protected function postRead() { }
	protected function postUpdate() { }
	protected function postDelete() { }
	protected function postUpdateOrCreate() { }
}
